FlutterでFirebase Realtime Databaseを使ってみた
大阪オフィスの山田です。AmazonでのEssential phoneのセールを見逃して泣きそうでした。今回はFirebase Realtime DatabaseをFlutterから使用してみたいと思います。Firebase Realtime Databaseを使うこと自体初めてです。
今回やること
Firebase Realtime Databaseに対して、一覧取得、追加をFlutterで作ったアプリから行います。ライブラリ、firebase_databaseを使用します。Exampleも存在しますが、今回はもっと簡潔にした実装をしていきます。イメージとしてはこのようなアプリです。
入力したら、リストに追加されていくだけのシンプルなチャットです(果たしてチャットと呼んで良いか疑わしいレベル)。もちろん、他の端末から見ると、投稿がリストに表示されるようにします。
開発環境
flutter doctor
Doctor summary (to see all details, run flutter doctor -v): [✓] Flutter (Channel beta, v0.5.1, on Mac OS X 10.13.5 17F77, locale ja-JP) [✓] Android toolchain - develop for Android devices (Android SDK 28.0.1) [✓] iOS toolchain - develop for iOS devices (Xcode 9.4) [✓] Android Studio (version 3.1) ✗ Flutter plugin not installed; this adds Flutter specific functionality. ✗ Dart plugin not installed; this adds Dart specific functionality. [!] VS Code (version 1.26.1) [✓] Connected devices (1 available)
flutter --version
Flutter 0.5.1 • channel beta • https://github.com/flutter/flutter.git Framework • revision c7ea3ca377 (3 months ago) • 2018-05-29 21:07:33 +0200 Engine • revision 1ed25ca7b7 Tools • Dart 2.0.0-dev.58.0.flutter-f981f09760
準備
各プラットフォームの設定
Android、iOS、各プラットフォームのプロジェクトをFirebase上に作成し、google-service.json
、GoogleService-Info.plist
をプロジェクトに追加します。こちらの記事の「準備」の章で各プラットフォームで設定をしていますのでご参照ください。
Firebase Realtime Databaseの準備
Databaseを用意します。Firebase Consoleにログインして、Databaseを選択します。
Realtime Databaseまでスクロールして、「データベースを作成」を選びます。
テストモードで作成し、誰でも読み書きできるようにします。
データベースが作成されました。
実装
データ構造を定義
まず、データ構造を定義します。1つのチャットメッセージはmessage
と投稿日時date
を持つようにします。それとは別にデータ上のキーを持ちます。簡単。以下のように定義しました。databaseのデータはDataSnapshot
として取得できるので、DataSnapshot
からインスタンスを生成できるようにしています。
class ChatEntry { String key; DateTime dateTime; String message; ChatEntry(this.dateTime, this.message); ChatEntry.fromSnapShot(DataSnapshot snapshot): key = snapshot.key, dateTime = new DateTime.fromMillisecondsSinceEpoch(snapshot.value["date"]), message = snapshot.value["message"]; toJson() { return { "date": dateTime.millisecondsSinceEpoch, "message": message, }; } }
画面の実装
画面全体の実装をまず記載しておきます。
class FirebaseChatPage extends StatefulWidget { @override _FirebaseChatPageState createState() => new _FirebaseChatPageState(); } class _FirebaseChatPageState extends State<FirebaseChatPage> { final _mainReference = FirebaseDatabase.instance.reference().child("messages"); final _textEditController = TextEditingController(); List<ChatEntry> entries = new List(); // チャッtのメッセージリスト @override initState() { super.initState(); _mainReference.onChildAdded.listen(_onEntryAdded); } _onEntryAdded(Event e) { setState(() { entries.add(new ChatEntry.fromSnapShot(e.snapshot)); }); } // 画面全体のビルド @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: new Text("Firebase Chat") ), body: Container( child: new Column( children: <Widget>[ Expanded( child: ListView.builder( padding: const EdgeInsets.all(16.0), itemBuilder: (BuildContext context, int index) { return _buildRow(index); }, itemCount: entries.length, ), ), Divider(height: 4.0,), Container( decoration: BoxDecoration(color: Theme.of(context).cardColor), child: _buildInputArea() ) ], ) ), ); } // 投稿されたメッセージの1行を表示するWidgetを生成 Widget _buildRow(int index) { return Card( child: ListTile( title: Text(entries[index].message) ) ); } // 投稿メッセージの入力部分のWidgetを生成 Widget _buildInputArea() { return Row( children: <Widget>[ SizedBox(width: 16.0,), Expanded( child: TextField( controller: _textEditController, ), ), CupertinoButton( child: Text("Send"), onPressed: () { _mainReference.push().set(ChatEntry(DateTime.now(), _textEditController.text).toJson()); _textEditController.clear(); // キーボードを閉じる FocusScope.of(context).requestFocus(new FocusNode()); }, ) ], ); } }
Realtime databaseからの受信部分
final _mainReference = FirebaseDatabase.instance.reference().child("messages"); // 1 @override initState() { super.initState(); _mainReference.onChildAdded.listen(_onEntryAdded); // 2 } _onEntryAdded(Event e) { // 3 setState(() { entries.add(new ChatEntry.fromSnapShot(e.snapshot)); }); }
- まず、databaseのルートの下の
messages
という子を参照するようにします。 messages
に子が追加されたら_onEntryAdded
をコールするように設定します。_onEntryAdded
で追加された子を取得できるのでSnapshotからChatEntry
クラスを生成してリストに追加して、画面を再描画します。
Realtime databaseへの送信部分
// 投稿メッセージの入力部分のWidgetを生成 Widget _buildInputArea() { return Row( children: <Widget>[ SizedBox(width: 16.0,), Expanded( child: TextField( controller: _textEditController, ), ), CupertinoButton( child: Text("Send"), onPressed: () { _mainReference.push().set(ChatEntry(DateTime.now(), _textEditController.text).toJson()); // 1 _textEditController.clear(); // キーボードを閉じる FocusScope.of(context).requestFocus(new FocusNode()); }, ) ], ); }
- 送信ボタンが押された時に、textEditControllerから入力されたテキストを取得し、現在時刻と共に
ChatEntry
クラスを生成して、送信します。
投稿した後、Firebase ConsoleでDatabaseを見ると以下のようになっていました。
ちゃんとデータが書き込まれていますね。
リストについて
今回の実装ではListView
を仕様していますが、今回使用したライブラリにはFirebaseAnimatedList
というクラスが用意されています。こちらはRealtime Databaseのリファレンスをセットしておくと、追加、更新、削除の時に自動で描画してくれる便利なクラスです。仕様に合致する場合は積極的に使って良いと思います。
最後に
いかがだったでしょうか。Flutterを触っていて詰まった時、iOSであればあたりがつくのですが、Androidの方は経験不足からなかなかどこに問題があるのかパッとイメージがつかず四苦八苦しております。これからもがんばろー。